/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
    ArkField,
    Stmt,
    ArkMethod,
    ArkAssignStmt,
    ArkInstanceFieldRef,
    ClassSignature,
    ArkStaticInvokeExpr, ArkInvokeStmt, AbstractInvokeExpr, FullPosition
} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from "../BaseChecker";
import {ClassMatcher, MatcherCallback, MatcherTypes, MethodMatcher} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {CheckerUtils} from "../../utils/checker/CheckerUtils";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeCallCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: "docs/no-unsafe-call-check.md", // TODO: support url
    description: "Disallow calling a function with a value with type `any`"
};

export class NoUnsafeCallCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };

    private buildMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.clsMatcher],
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildTs: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        }
        return [matchBuildTs];
    }

    public check = (targetMethod: ArkMethod) => {
        const stmts = targetMethod.getBody()?.getCfg().getStmts() ?? [];
        if (!this.getFileExtension(targetMethod.getDeclaringArkClass().getDeclaringArkFile().getName(), 'ts')) {
            return;
        }
        for (const stmt of stmts) {
            if (!this.containsParentheses(stmt.getOriginalText() || '')) {
                continue;
            }
            if (stmt instanceof ArkAssignStmt) {
                if ((stmt.getRightOp() as ArkInstanceFieldRef)?.getBase) {
                    if ((stmt.getRightOp() as ArkInstanceFieldRef)?.getBase().getType().getTypeString().includes("any")) {
                        this.addIssueReport(stmt);
                    }
                }
                if ((stmt.getRightOp() as ArkInstanceFieldRef)?.getFieldSignature) {
                    let classSignature = (stmt.getRightOp() as ArkInstanceFieldRef)?.getFieldSignature().getDeclaringSignature();
                    if (classSignature instanceof ClassSignature) {
                        let invokeClass = targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClass(<ClassSignature>classSignature)
                        if (invokeClass) {
                            invokeClass.getFields().forEach(field => {
                                if (field.getType().getTypeString().includes("any")) {
                                    this.addIssueReport(stmt);
                                }
                            });
                        }
                    }
                }
                if (stmt.getRightOp() instanceof ArkStaticInvokeExpr) {
                    this.getInvokeExprReport(stmt, targetMethod, (stmt.getRightOp() as ArkStaticInvokeExpr))
                }
            }
            if (stmt instanceof ArkInvokeStmt) {
                const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
                if (invokeExpr) {
                    this.getInvokeExprReport(stmt, targetMethod, invokeExpr)
                }
            }
        }
    }

    private getInvokeExprReport(stmt: Stmt, targetMethod: ArkMethod, invokeExpr: AbstractInvokeExpr | ArkStaticInvokeExpr) {
        let methodName = invokeExpr?.getMethodSignature().getMethodSubSignature().getMethodName();
        if (methodName === 'constructor') {
            methodName = invokeExpr?.getMethodSignature().getDeclaringClassSignature().getClassName();
        }
        targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClasses().forEach(invokeClazz => {
            invokeClazz.getFields().forEach((field: ArkField) => {
                if (field.getName() === methodName) {
                    if (field.getType().getTypeString().includes("any")) {
                        this.addIssueReport(stmt);
                    }
                }
            })
        });
        let method: ArkMethod = targetMethod.getDeclaringArkClass().getDeclaringArkFile().getClasses()[0].getMethods()[0];
        let bodyLocal = method.getBody()?.getLocals().get(methodName || '');
        if (bodyLocal?.getType().getTypeString().includes("any")) {
            this.addIssueReport(stmt);
        }
    }

    private findFirstParenthesesIndex(input: string): number {
        if (input.includes('new')) {
            return input.indexOf(')');
        } else {
            return input.indexOf('(');
        }
    }

    private containsParentheses(input: string): boolean {
        const regex = /\(|\)/;
        return regex.test(input);
    }

    private getFileExtension(filePath: string, filetype: string): boolean {
        const match = filePath.match(/\.([0-9a-zA-Z]+)$/);
        if (match) {
            const extension = match[1];
            return extension === filetype;
        }
        return false;
    }

    private addIssueReport(stmt: Stmt) {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(stmt);
        const fixKey = warnInfo.line + '%' + warnInfo.startCol + '%' + warnInfo.endCol + '%' + this.rule.ruleId;
        const mergeKey = warnInfo.filePath + '%' + fixKey + '%' + this.metaData.description;
        let defect = new Defects(warnInfo.line, warnInfo.startCol, this.metaData.description, severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey);
        this.issues.push(new IssueReport(defect, undefined));
    }

    private getLineAndColumn(stmt: Stmt) {
        const originalPositions = stmt.getOperandOriginalPositions();
        let parenthesesIndex = this.findFirstParenthesesIndex(stmt.getOriginalText() || '');
        let position: FullPosition | undefined = undefined;
        if (originalPositions![1]) {
            position = originalPositions![1];
        } else {
            position = originalPositions![0];
        }
        if (position) {
            const line = position.getFirstLine();
            const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
            if (arkFile) {
                let startCol = position.getFirstCol();
                const endCol = parenthesesIndex + 1;
                const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
                return {line, startCol, endCol, filePath: filePath}
            } else {
                logger.debug('originStmt or arkFile is null');
            }
        }
        return {line: -1, startCol: -1, endCol: -1, filePath: ''};
    }
}